<?php

namespace app\service;

use yii\helpers\ArrayHelper;
use yii\web\UploadedFile;

/**
 * 文件上传
 * Class UploadFileModel
 * @package app\helper
 * 说明：请自行补充
 *      静态成员变量 - $_fileType：允许文件列表二维数组，文件类型(外部初始化) => 文件类型 => 文件头大小(放置而已上传木马)
 *      成员方法 - validXXXFile：只有在新加验证[$_fileType]文件类型(外部初始化)的列表的时候需要新加，查看已有的验证方法仿照写即可
 */
final class UploadFileService extends BaseService
{

    /**
     * 图片类型
     */
    const imageType = 'image';
    /**
     * 视频类型
     */
    const videoType = 'video';
    /**
     * 音频类型
     */
    const audioType = 'audio';
    /**
     * 3D类型
     */
    const threeDType = 'three_d';
    /**
     * Office文件类型([word]、[excel]等)
     */
    const officeType = 'office';
    /**
     * Excel文件类型
     */
    const excelType = 'excel';

    /***************************
     * 系统规定上传文件参数不能修改 *
     ***************************/
    /**
     * 上传大小限制
     * @var int
     */
    protected $_fileMaxSize = 5242880; // 文件大小默认5m 可以自定义 但是要用 $file_max_size
    /**
     * 实例
     * @var self
     */
    protected static $uploader;
    /**
     * 默认允许文件类型列表
     * @var array
     */
    protected static $_fileType = [
        self::imageType  => [
            'jpg'  => 255216,
            'png'  => 13780,
            //            'bmp' => 0,
            'jpeg' => 255211,
            'gif'  => 7173
        ],
        self::videoType  => [
            'mp4' => 0, // 视频大小取不出来
        ],
        self::audioType  => [
            'mpeg' => 7368, // mp3竟然是这种格式!??
        ],
        self::threeDType => [
            'octet-stream' => 103108
        ],
        self::officeType => [
            // xlsx
            'vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 8075,
            // xls
            'vnd.ms-excel' => 208207
        ],
        self::excelType => [
            // xlsx
            'vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 8075,
            // xls
            'vnd.ms-excel' => 208207
        ]
    ];

    /************************
     * 用户自定义参数可传入修改 *
     ************************/
    /**
     * 自定义文件路径 - 项目任意文件夹下
     * @var
     */
    public $filePath;
    /**
     * 操作文件类型 格式：请参考[$_fileType]的[key]
     * @var
     */
    public $fileType;
    /**
     * 文件大小 可以不填写
     * @var
     */
    public $fileMaxSize;
    /**
     * 文件尺寸类型
     *  PS：horizontal|hor|1 - 水平；vertical|ver|2 - 垂直
     * @var
     */
    public $imgSizeType;
    /**
     * 是否自动重命名(防止重复)
     * @var bool
     */
    public $isRandFileName = false;
    /**
     * 文件上传信息
     * @var
     */
    public $uploadInfo;
    /**
     * 文件名前缀
     * @var string
     */
    public $filePrefix = '';
    /**
     * 文件名
     * @var
     */
    public $fileName;
    /**
     * 最终file列表信息
     * @var UploadedFile|array
     */
    public $fileInfos;

    /**
     * 验证规则
     * @return array
     */
    public function rules()
    {

        return [
            // 必须：
            [['filePath', 'fileType', 'fileInfos'], 'required'],
            [['filePrefix'], 'string'],
            // 类型：
            [['fileMaxSize'], 'integer'],
            ['isRandFileName', 'in', 'range' => [true, false]],
            [['fileInfos'], 'validFile']
        ];
    }

    /**
     * 标签
     * @return array
     */
    public function attributeLabels()
    {

        return [
            'fileMaxSize'    => '上传文件大小',
            'fileType'       => '允许上传文件类型',
            'fileName'       => '文件名',
            'fileInfos'      => '文件信息',
            'filePath'       => '文件保存路径',
            'isRandFileName' => '是否重命名',
        ];
    }

    /**
     * 验证文件
     * @param $attribute
     * @param $params
     * @return mixed
     */
    public function validFile($attribute, $params)
    {

        foreach ($this->$attribute as $k => $v) {

            if ($v->error != 0) {

                $this->addError(500, self::getInstanceError($v->error));
                return false;
            }

            ### 文件[类型]
            $fp  = fopen($v->tempName, "rb");
            $bin = fread($fp, 2); //只读2字节
            fclose($fp);
            $str_info  = @unpack("C2chars", $bin);
            $type_code = intval($str_info['chars1'] . $str_info['chars2']);

            // 根据[即将上传的文件类型]、[当前文件头大小]取出合法的文件类型
            $fileType = self::getTypeSizeForType($this->fileType, $type_code);
            // 未找到就是不合法
            if (!$fileType) {

                $this->addError('400', '文件类型不合法：' . $this->fileType);
                return false;
            }

            // 分割文件MimType验证类型
            // 分割文件扩展名
            $fileType     = explode('/', $v->type);
            $fileType     = empty($fileType[1]) ? 'unknown' : $fileType[1];
            $fileTypeList = empty(self::$_fileType[$this->fileType]) ?
                [] : array_keys(self::$_fileType[$this->fileType]);
            if (!in_array($fileType, $fileTypeList)) {

                $this->addError('400', '文件的类型不合法：' . $v->type);
                return false;
            }

            ### 文件[大小]
            $stageFSize = $v->size;
            // 小于1Kb
            if ($stageFSize < 1) {

                $this->addError('400', '文件大小不合法: ' . $stageFSize . '字节');
                return false;
            }
            // 大于规定Kb
            if ($stageFSize > $this->fileMaxSize) {

                $this->addError('400', '文件大小不合法: ' . $stageFSize . '字节');
                return false;
            }

            ### 图片文件[尺寸类型]
            if (!empty($this->imgSizeType) && $this->imgSizeType && $this->fileType == self::imageType) {

                $fileSize = getimagesize($v->tempName);
                // 横屏
                if (in_array($this->imgSizeType, [1, 'hor', 'horizontal']) &&
                    $fileSize[0] <= $fileSize[1]
                ) {
                    $this->addError(400, '请上传横屏照片');
                    return false;
                }
                // 竖屏
                if (in_array($this->imgSizeType, [2, 'ver', 'vertical']) &&
                    $fileSize[0] >= $fileSize[1]
                ) {
                    $this->addError(400, '请上传竖屏照片');
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * 检测：是否有文件
     */
    public function hasFile()
    {
        // 是否存在这个文件
        if (empty($file_name)) {
            return false;
        }
        $file_info  = UploadedFile::getInstanceByName($this->file_name);
        $file_infos = UploadedFile::getInstancesByName($this->file_name);
        if (empty($file_info) && empty($file_infos)) {
            return false;
        }

        return true;
    }

    /**
     * 初始化
     * @param $file [文件File名称]
     * @param array $config [配置选项]
     * @return self
     */
    public static function loadModel($file, $config = [])
    {
        return self::getUploader()->loadConfig($config)->initUpload($file);
    }

    /**
     * 获取实例
     * @return self
     */
    protected static function getUploader()
    {

        if (empty(self::$uploader)) self::$uploader = new self();

        return self::$uploader;
    }

    /**
     * 初始化一些默认信息|example：文件类型限制
     * @param string $file [文件FILE[name]]
     * @return self
     */
    protected function initUpload($file = '')
    {

        $instance = self::getUploader();
        // 先判断下是否有文件这个最重要
        $fileList = UploadedFile::getInstancesByName($file);

        // 允许上传的文件类型
        $fileTypes = $instance::getAllowTypes($instance->fileType);
        if (empty($fileTypes)) {
            $instance->addError('fileType', '请设置允许文件类型，请检查[fileType]设置');
            return $instance;
        }

        // 下面是过滤非[规定]文件的
        foreach ($fileList as $k => $v) {

            $stageNowType = explode('/', $v->type);

            // 输出查看文件类型
            //            var_dump(
            //                strtolower($stageNowType[1])
            //            );die;
            // 类型获取失败下一条
            if (empty($stageNowType[1]) || !in_array(strtolower($stageNowType[1]), $fileTypes)) {
                unset($fileList[$k]);
                continue;
            }
        }

        // [文件信息不能为空。]可以去上面循环中提取出文件类型，文件列表里完善相关文件类型大小值

        // 赋值允许文件类型列表|去空/去重
        $instance->loadConfig([
            'fileInfos'   => $fileList,
            'fileMaxSize' => $instance->_fileMaxSize
        ]);

        return $instance;
    }

    /**
     * 载入某些配置
     * @param $config
     * @return self
     */
    public function loadConfig($config)
    {

        $uploader = self::getUploader();

        foreach ($config as $k => $v) {

            if (!$uploader->hasAttribute($k)) continue;

            $uploader->setAttributes([
                $k => $v
            ], false);
        }

        return $uploader;
    }

    /**
     * 上传文件
     * @return bool
     */
    public function upload()
    {

        // 先验证
        if ($this->hasErrors() || !$this->validate()) {
            return false;
        }

        //*********** 监测保存文件夹是否存在 *************//
        // 清空
        $this->uploadInfo = [];
        // 路径：
        $paths = $this->filePath;
        // 查看有没有这个文件夹 | 没有创建：
        $mkdirResult = self::mkdir($paths);
        // 文件夹创建失败
        if ($mkdirResult !== true) {

            $this->addError('file_path', $mkdirResult);
            return false;
        }

        // ********** 文件信息 ***************
        // 基本base文件信息
        $fileInfoArray = $this->fileInfos;
        foreach ($fileInfoArray as $k => $fileInfo) {

            /* @var UploadedFile $fileInfo */
            // 文件类型，example：image,video等等
            $stageMimeType = explode('/', $fileInfo->type)[0];

            //*********** 随机文件名 *************//
            if ($this->isRandFileName) {
                $randName = $this->renderName();
            } else {
                $randName = $fileInfo->baseName;
            }

            // 文件最终路径 + 名称 + 扩展名
            $finalPathName = $paths . '/' . $randName . "." . strtolower($fileInfo->extension);

            // 保存结果：
            $save_result = $fileInfo->saveAs($finalPathName);

            if (!$save_result) {

                $this->addError('save error', '文件保存失败');
                $this->uploadInfo = [
                    'name'     => $randName . "." . strtolower($fileInfo->extension),
                    'path'     => $finalPathName,
                    'full'     => $finalPathName,
                    'mimeType' => $fileInfo->type, // example: image/jpeg
                    'fileType' => $stageMimeType, // example: image,video
                    'fileExt'  => strtolower($fileInfo->extension), // example: jpg|png
                    'size'     => $fileInfo->size, // 文件大小
                    'result'   => false,
                    'width'    => 0,
                    'height'   => 0
                ];

                continue;
            }

            $this->uploadInfo = [
                'path'     => $finalPathName,
                'name'     => $randName . "." . $fileInfo->extension,
                'full'     => $finalPathName,
                'mimeType' => $fileInfo->type, // example: image/jpeg
                'fileType' => $stageMimeType, // example: image,video
                'fileExt'  => strtolower($fileInfo->extension), // example: jpg|png
                'size'     => $fileInfo->size, // 文件大小
                'result'   => true,
            ];
            // 图片类型添加图片信息
            if ($this->fileType == self::imageType) {
                $imageSize        = getimagesize($finalPathName);
                $this->uploadInfo = array_merge($this->uploadInfo, [
                    'width'  => $imageSize['0'],
                    'height' => $imageSize['1']
                ]);
            }
        }

        // 最终循环失败的记录删除
        $this->delFailFile();

        if (count($this->uploadInfo) < 1) {
            return false;
        }

        return true;
    }

    /**
     * 获取上传的文件信息|支持去掉某路径|如果是多个文件的话就返回多维数组格式，不是就返回一维数组
     * @return mixed
     */
    public function getUploadInfo()
    {

        if (!$this->uploadInfo) return null;
        $basePath = \Yii::getAlias('@webroot');

        if (!is_array(current($this->uploadInfo))) {

            $this->uploadInfo['path'] = str_replace($basePath,
                '',
                empty($this->uploadInfo['path']) ? '' : $this->uploadInfo['path']);

            return $this->uploadInfo;
        }

        foreach ($this->uploadInfo as $k => &$v) {

            $v['path'] = str_replace($basePath,
                '',
                empty($v['path']) ? '' : $v['path']);
        }

        return $this->uploadInfo;
    }

    /**
     * 创建一个目录
     * @param $path string 全路径
     * @return bool|string true 文件创建成功或者已经存在并且可以写入
     *                     string 文件存在没有权限或者创建失败
     */
    protected static function mkdir($path)
    {
        // ☆ 文件夹不存在
        if (!is_dir($path)) {
            try {
                mkdir($path, 0777, true);
                return true;
            } catch (\Exception $e) {
                return $e->getMessage() . ' 错误路径： ' . $path;
            }
        }

        // ☆ 文件夹可写入
        if (is_writable($path)) {
            return true;
        }

        // ☆ 文件夹不可写入 && 文件夹不存在
        if (strstr($path, \Yii::getAlias('@app'))) {
            // 为了安全替换掉项目目录以外的路径
            $path = str_replace(\Yii::getAlias('@app'), '', $path);
            $path = '项目路径' . $path;
        }
        // 返回错误信息
        return '该目录已经存在且没有写入权限！【' . $path . '】';
    }

    /**
     * 生成随机名字
     * @return string
     */
    protected function renderName()
    {

        return $this->filePrefix .
            '_T_' .
            time() .
            '_T_' .
            mt_rand(10000, 99999) .
            substr(md5($this->getRandomStr()), 0, 3);
    }

    /**
     * 生成一个N位随机字符
     * @param int $len
     * @return string
     */
    public static function getRandomStr($len = 24)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $final = '';
        for ($i = 0; $i < $len; $i++) {

            $final .= $chars[mt_rand(0, strlen($chars) - 1)];
        }
        return $final;
    }

    /**
     * 根据文件类型(请参考参数[$_fileType]的[KEY])返回允许的文件类型
     * @param $type
     * @return array|mixed
     */
    public static function getAllowTypes($type)
    {

        if (empty(self::$_fileType[$type])) return [];

        return array_keys(self::$_fileType[$type]);
    }

    /**
     * 文件类扩展名称是否允许
     * @param string $ext 扩展名
     * @return array|mixed
     */
    public static function isAllowTypes($ext)
    {

        // 扩展名为空
        if (empty($ext)) return false;

        $allowTypes = [];
        foreach (array_values(self::$_fileType) as $k => $v) {
            $allowTypes = ArrayHelper::merge($allowTypes, $v);
        }

        return isset($allowTypes[$ext]);
    }

    /**
     * 根据文件类型(请参考参数[$_fileType]的[KEY])、文件头大小 返回文件类型
     * @param string|int $type 允许的文件类型
     * @param int|float $size 文件头大小
     * @return bool
     */
    public static function getTypeSizeForType($type, $size)
    {

        if (empty(self::$_fileType[$type])) return false;

        $list = array_flip(self::$_fileType[$type]);

        if (empty($list[$size])) return false;

        return $list[$size];
    }

    /**
     * 根据文件扩展名(请参考参数[$_fileType])、文件头大小 返回文件类型
     * @param string|int $ext 允许的文件扩展名
     * @param int|float $size 文件头大小
     * @return bool|int
     */
    public static function getTypeSizeForExt($ext, $size)
    {

        // 扩展名为空
        if (empty($ext)) return false;

        $allowTypes = [];
        foreach (array_values(self::$_fileType) as $k => $v) {
            $allowTypes = ArrayHelper::merge($allowTypes, $v);
        }

        return isset($allowTypes[$ext]) ? $allowTypes[$ext] : false;
    }

    /**
     * 获取实例失败错误
     * Detail to see：https://www.php.net/manual/en/features.file-upload.errors.php
     * @param $code
     * @return mixed|string
     */
    protected function getInstanceError($code)
    {

        $errList = [
            1 => '获取文件实例错误：文件大小超过限制，请修改配置文件大小限制',
            2 => '获取文件实例错误：文件大小超过HTML的`MAX_FILE_SIZE `',
            3 => '获取文件实例错误：文件内容不完全',
            4 => '获取文件实例错误：上传文件为空',
            6 => '获取文件实例错误：缺少临时文件夹', // . Introduced in PHP 5.0.3
            7 => '获取文件实例错误：无法将文件写入磁盘', // . Introduced in PHP 5.1.0
            8 => '获取文件实例错误：一个PHP扩展停止了文件上传' // . Introduced in PHP 5.2.0
        ];

        if (!empty($errList[$code]))

            return $errList[$code];
        else
            return '未知实例错误：' . $code;
    }

    /**
     * 删除失败的文件上传结果中文件
     */
    private function delFailFile()
    {

        if (!$this->uploadInfo) return false;

        $info = $this->uploadInfo;
        // 看是否是二维数组
        if (!empty($info['full'])) $info = [$info];
        foreach ($info as $k => $v) {
            if (empty($v['result']) || !$v['result']) {
                unlink($v['full']);
            }
            unset($this->uploadInfo[$k]);
        }

        return true;
    }
}
